代码逆向基础

关于逆向工程

分析方法:

静态分析与动态分析,二者结合相辅相成。

  • 静态看类型、PE文件信息、壳信息、字符串、API + IDA Pro源码
  • 动态看行为监控 + 动态调试

先静态-推测程序结构与行为机制,再动态-找切入点,如DLL的main、关键API调用等。

两个概念:

  • patch:打补丁,即对应用程序文件或者进程内存进行修改
  • Crack:破解,与Patch类似,但是意图非法

前者目的在于修复漏洞、后者在于破解软件;而二者应用的技术就是逆向工程。而学习的内容是逆向技术原理OS内部体系结构

简单逆向分析

以OD为例,简单实例分析。

入口点

当使用OD打开可执行文件后,会跳转到EP位置进行执行。需要注意的是这个地址不是main函数,而某些程序的启动代码,这些代码随着编译器的不同而变化。

回顾一下基础的指令

  • Ctrl+G:地址的跳转,包括代码窗口与内存窗口
  • F2:断点(软)
  • Ctrl+F2:重新加载调试
  • F4:运行到指定位置。利用的是硬件断点
  • F7:单步步入
  • F8:单步步过
  • F9:多步步过(到下一个断点)
  • Ctrl+F9:执行到返回
  • :添加标签(修改替换)
  • :添加注释
  • *:回到EIP
  • Enter:查看call、jmp后续
  • ALT+B:查看断点窗口
  • ALT+L:查看日志窗口
  • Ctrl+E:数据窗口快速编辑

下面介绍几种快速定点的方法

所谓的快速定点就是在调试过程中添加人为标记,便于整个流程分析与复盘。

  1. Ctrl+G AND F4

    上面的指令都有涉及到,前者用于地址的跳转,后者用于执行到光标位置。所以二者结合可以实现在某个位置开始调试。

    image-20230915131948649
  2. 设置断点(后续具体介绍,是最常用的一种调试手段)

    断点的种类很多,比如硬件断点、软件断点、内存断点、条件断点等,设置断点的方法也很多,比如在某位置使用F2或者通过Ctrl+N利用导入表设置;使用ALT+B可以打开断点窗口,查看或跳转到某断点。

    image-20230915133432002
  3. 添加注释

    通过使用可以在指定位置添加注释,而后使用右键查找-用户注释即可查看。

    image-20230915134008932
  4. 设置标签

    可以通过设置标签,即在指定地址添加特定的名称。也可以在上图位置进行查看设置的所有标签。

下面介绍几种快速查找指定代码的方法:以查找messageBox所在的main函数为例。

  1. 极限F8法

    适用场景:功能明确,代码简单

    具体方法:确定大本营后,连续使用F8,直到跳出窗口,则该函数就是main函数,可以设置标记(断点、标签等)

  2. 字符串检索法

    OD在加载文件时会先进行预分析,将使用到的字符串、API等单独列出,使用可以通过这种方法查询指定字符串。如下图,通过查看helloworld字符串即可定位到main函数。

    image-20230915135401523

  3. API检索法

    通过查看该程序的函数调用列表,找到messageBox函数。

    image-20230915135748091

  4. 模块与导入函数法

    通过右键-查找-所有模块,之后通过名称查询即可。

    image-20230915140355811

前面介绍了如何设置定位点、如何查找指定API,下面学习在找到指定代码后如何进行修改

  1. 直接修改内存内容

    通过Ctrl+G跳转到指定的内存区域,使用Ctrl+E进行编辑。注意要加00结尾。

    image-20230915141253299 image-20230915141309494
  2. 修改传递参数。

    新建一个字符串,之后修改messageBox的参数即可,使用空格修改汇编代码

    image-20230915142022834

字节端序

什么是字节序呢?简单来说就是多字节数据在内存或者网络中的排序方式

可以分为两种字节序:

  • 大端序:内存低位地址存储数据的高位,多是网络协议中使用。
  • 小端序:内存低地址存储数据的低位,多少Intel x86CPU使用,也是后面学习使用的。

怎么记忆呢?记住一种即可:高对高逆序存储 = 契合 = 广泛 = 小端

IA-32寄存器学习

调试过程中涉及的寄存器很多,在不断学习中不断总结。本次学习基本程序运行寄存器

其他寄存器:内存寄存器、调试寄存器、控制寄存器,非IA-32架构的寄存器,后续学习都会涉及。

简单分类

  • 通用寄存器:用于数据的存储与传输。一共8个:eax、ebx、ecx、edx、esp、ebp、esi、edi
  • 段寄存器:用于存储段描述符,与SDT一起完成虚拟内存的构建与段机制的完成。一共6个,16位:CS、DS、SS、ES、FS、GS
  • 标志寄存器:用于标识状态信息,32位、按位起作用。
  • 指令指针寄存器:存储下一条指令的地址,32位、EIP。
image-20230915154939168

细致介绍

  1. 通用寄存器eax、ebx、ecx、edx、esp、ebp、esi、edi

    概况:最常用的几个寄存器,要熟悉其用法。

    具体功能介绍

    • EAX:累加器add。多用于保存函数返回结果。

    • EBX:基址寄存器base。多用于记录数据基址。

    • ECX:计数器count。多用于计数,比如循环中使用其保存i。

    • EDX:数据寄存器data。血统最纯,记录重要数据。

    • ESP:栈顶指针寄存器,保存栈顶指针,很重要,在函数调用、栈帧机制中。

    • EBP:扩展基址指针寄存器,用于SS表示的栈区,记录某个栈帧的基址。

    • ESI:源地址寄存器,多用于内存复制。

    • EDI:目的指针寄存器,多用于内存复制。

  2. 段寄存器:CS、SS、DS、ES、FS、GS

    概况:16位寄存器,存储的不是段的地址,而是SDT的索引。(SDT是段描述符表,下面有介绍)

    具体介绍

    • CS:代码段寄存器

    • SS:栈段寄存器

    • DS:数据段寄存器

    • ES:附加段寄存器

    • FS:数据段寄存器,这个寄存器很重要,后续学习SEH(异常处理机制)TEB、PEB(环境块)时会用到。

    • GS:数据段寄存器

    扩展:段描述符表(Segment Descriptor Table)

    段描述符表(Segment Descriptor Table)是指在x86架构的计算机体系结构中使用的数据结构,用于管理和描述内存中的各个段(segment)。

    ​ 在x86架构中,内存被划分为多个段,每个段具有不同的权限和属性。段描述符表存储了这些段的描述符,每个描述符包含了段的基地址(base address)段的大小(segment size)访问权限(access rights)其他属性信息

    ​ 段描述符表通常是一个数组或者表格的形式,每个表项对应一个段描述符。处理器使用段选择子(Segment Selector)来索引段描述符表,从而找到对应的段描述符。当程序引用一个内存地址时,处理器会根据段选择子找到对应的段描述符,然后使用其中的信息来执行相应的访问控制和内存管理操作。

    ​ 需要注意的是,自从Intel引入了64位的x86架构(x86-64或AMD64),段描述符表的使用已经不再是必须的,因为64位模式下的内存管理采用了平坦模型Flat Model),即所有的内存被视为单一的连续空间,不再划分为不同的段。然而,段描述符表仍然在兼容性模式下使用,以支持32位的应用程序和操作系统。

    补图:

    image-20230915161314878
  3. 标志寄存器:

    概况:这个寄存器用于记录状态信息,是以bit为单位的。

    功能:不需要记住所有的标志位含义,只需要知道其中的三个。

    • ZF:zero flag,零标志位,若该寄存器为1,则表示结果为0.

    • OF:overflow flag,溢出标志位,若该值为1,则表示有符号整数溢出。

    • CF:Carry flag,进位标志位,若该值为1,则表示无符号整数溢出。

    歌曰:标志位寄存器经常与条件跳转关联,比如JNZ、JZ等,根据ZF标志位的结果决定是否跳转;而在判断之前会通过test、cmp等指令为标志位赋值

  4. 指令指针寄存器:EIP,存储下一条指令的地址。

栈与栈帧

理清关键概念定义,用自己的话。

当曰:什么是栈?

歌曰:栈是一种数据结构,遵循先入后出的数据存储原则。具体到虚拟内存空间,栈是其中一段连续的地址,向着低地址方向增长,通过PUSHPOP实现压栈和弹栈。


当曰:那栈有什么用嘞?

歌曰:那当然有用。栈与程序的执行过程密切相关,因为程序就是各个函数的组合,而每一个函数的状态变化便是通过栈记录的。具体来说,栈有以下三个作用

  1. 参数的传递:在函数执行之前,会先传递参数,以供函数内部调用。(64位程序采取寄存器传参)
  2. 状态的记录(上下文信息):在函数执行完毕后要返回,所以要知道函数执行前的状态信息,比如关键寄存器的数据、要返回的地址等。
  3. 局部变量的存储:局部变量只有在函数内部起作用,如何实现呢,只需要在栈里创建该变量即可,不需要在数据段。

当曰:什么是栈帧?

歌曰:栈帧可以看作是栈区一小段空间,每一个栈帧都对应一个函数调用。当函数调用时要先开辟栈帧,由EBP指向栈帧底部、ESP指向栈帧顶部。栈帧的作用就是管理函数调用过程中的参数、局部变量、返回值、上下文信息

当曰:如何管理呢?

歌曰:程序利用EBP寄存器访问栈内局部变量、参数、函数返回地址等信息。通过使用栈帧,计算机可以有效地管理函数调用和返回的过程,确保函数之间的数据隔离和正确的执行顺序

函数调用约定

函数调用约定就是指函数在指向过程中,参数如何被调用的约定。具体来说,无外乎两个方面,即参数如何传递函数结束后参数如何销毁

在介绍具体的调用约定之前,先来说一下当函数执行完毕后栈里的数据怎么处理?答案就是:不处理。因为处理是需要花费资源的,所以一般的做法是只改变EBP和ESP的值,即修改栈的大小;后续使用栈时直接覆盖即可。

下面具体说下目前使用最多的三种调用约定

  • cdeclC语言默认的调用约定,由调用者进行栈平衡操作,参数由栈进行传递。这种方法的好处是可以传递可变长度的参数,更加灵活,其栈平衡操作通常是调用者使用add esp,xx进行。
  • stdcallWIN32 API默认使用的调用约定,由被调用者进行栈平衡操作,参数由栈进行传递。这种方法的好处代码尺寸小,其栈平衡操作通过是使用RETN完成的,即ret与 pop N
  • fastcall:与stdcall类似,不同的是前面几个参数使用寄存器传递,所以执行速度更快。

后续学习

切记不要急躁,切记不要蜻蜓点水,切记不要眼高手低。

各个工具的使用、分析流程的梳理、PE文件格式与加载流程、进程注入与Hook、Windows内核与驱动、反汇编反调试反虚拟机反沙箱、脱壳与加壳、补丁与保护机制等等,每一个都不是只看看理论就可以掌握的,要细看、要书画、要思考、要实践、要总结、要记录、要回顾、要更新。

恶意样本中基本分析流程与细致分析方法、各个类型家族实践总结、WinAPI\zfc\dll\reg积累、常见的入侵行为如感染、启动、隐蔽、持久化、核心功能点、样本处置措施三个,对于这些更要多多实践分析,学无止境。

漏洞研究先看基础、内核,再看书、实践复现等。